package com.shigi.sat.ga;

import com.shigi.sat.structure.SatInstance;
import com.shigi.sat.structure.SatSolution;
import com.shigi.sat.SatSolver;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
 * Created by Miroslav Šiagi on 28/01/15.
 */
public class GASolver implements SatSolver {
    public GAConfig gaConfig;
    public SatPopulation satPopulation;

    private GAUtils gaUtils;
    private SatInstance originalInstance;
    private SatSolution satSolution;

    public GASolver(GAConfig gaConfig) {
        this.gaConfig = gaConfig;
        this.gaUtils = new GAUtils(this.gaConfig);
    }

    @Override
    public SatSolution solve(SatInstance satInstance) {
        this.originalInstance = satInstance;
        this.satSolution = new SatSolution(this.originalInstance.id);
        // TODO We need to get every unique solution from the evolution process or
        // the best individual of every population/generation eventually

        // We need to store every unique solution independently (within fitness value computation)
        // A progress of these solutions through the evolution process


        createPopulation();
        evolvePopulation();
        return this.satSolution;
    }

    private void evolvePopulation() {
        List<Double> bestFitnessValues = new ArrayList<>();
        List<SatIndividual> bestIndividuals = new ArrayList<>();

        int idOfBestGeneration = 0;

        SatIndividual currentBestIndividual = gaUtils.computeFitnessOnPopulation(this.satPopulation);
        SatIndividual bestIndividualEver = new SatIndividual(currentBestIndividual);

        bestFitnessValues.add(currentBestIndividual.fitnessValue);
        bestIndividuals.add(currentBestIndividual);

        while(terminationConditionNotSatisfied()) {
            sortIndividuals();
            this.satPopulation = createNewPopulation();
            this.satPopulation.numberOfGeneration++;

            currentBestIndividual = gaUtils.computeFitnessOnPopulation(this.satPopulation);
            bestFitnessValues.add(currentBestIndividual.fitnessValue);
            bestIndividuals.add(currentBestIndividual);

            if(currentBestIndividual.fitnessValue > bestIndividualEver.fitnessValue) {
                bestIndividualEver = new SatIndividual(currentBestIndividual);
                idOfBestGeneration = this.satPopulation.numberOfGeneration - 1;
            }
        }

        System.out.println("Fitness of the best individual: " + currentBestIndividual.fitnessValue);
        System.out.println("Id of the best generation: " + idOfBestGeneration);
    }

    private SatPopulation createNewPopulation() {
        SatPopulation newPopulation = new SatPopulation(this.satPopulation);
        int numOfOriginalIndivids = this.satPopulation.satIndividuals.length;
        int numOfNewIndivids = 0;

        while(numOfNewIndivids < numOfOriginalIndivids) {
            numOfNewIndivids = gaUtils.performReproductionOrCrossover(this.satPopulation, newPopulation, numOfNewIndivids);

            int difference = numOfOriginalIndivids - numOfNewIndivids;
            if(difference < 3 && difference > 0) {
                gaUtils.performMutation(newPopulation);
                // elitism
                for (int i = 0; i < difference; i++) {
                    newPopulation.satIndividuals[numOfNewIndivids++] = this.satPopulation.satIndividuals[i];
                }
            }
        }

        return newPopulation;
    }

    private void sortIndividuals() {
        Arrays.sort(this.satPopulation.satIndividuals, new Comparator<SatIndividual>() {
            @Override
            public int compare(SatIndividual o1, SatIndividual o2) {
                if (o1.fitnessValue < o2.fitnessValue) {
                    return 1;
                } else if (o1.fitnessValue == o2.fitnessValue) {
                    return 0;
                } else
                    return -1;
            }
        });
    }

    private boolean terminationConditionNotSatisfied() {
        return (this.satPopulation.numberOfGeneration < this.gaConfig.numOfGenerations);
    }

    private void createPopulation() {
        SatIndividual[] satIndividuals = createIndividuals();
        this.satPopulation = new SatPopulation(satIndividuals);
    }

    private SatIndividual[] createIndividuals() {
        SatIndividual[] satIndividuals = new SatIndividual[this.gaConfig.numOfIndividuals];
        for (int i = 0; i < this.gaConfig.numOfIndividuals; i++) {
            satIndividuals[i] = new SatIndividual(new SatInstance(this.originalInstance));
            this.gaUtils.setInitialValues(satIndividuals[i].satInstance);
        }

        return satIndividuals;
    }
}
